package tools.jackson.core.unittest.read;

import java.io.IOException;
import java.util.*;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.core.io.SerializedString;
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.json.JsonFactoryBuilder;
import tools.jackson.core.json.JsonReadFeature;
import tools.jackson.core.json.UTF8DataInputJsonParser;
import tools.jackson.core.unittest.*;

import static org.junit.jupiter.api.Assertions.*;

@SuppressWarnings("resource")
public class TrailingCommasTest extends JacksonCoreTestBase
{
    JsonFactory factory;
    Set<JsonReadFeature> features;
    int mode;

    public void initTrailingCommasTest(int mode, List<JsonReadFeature> features) {
      this.features = new HashSet<>(features);
      JsonFactoryBuilder b = (JsonFactoryBuilder) JsonFactory.builder();
      for (JsonReadFeature feature : features) {
          b = b.enable(feature);
      }
      factory = b.build();
      this.mode = mode;
  }

  public static Collection<Object[]> getTestCases() {
    ArrayList<Object[]> cases = new ArrayList<>();

    for (int mode : ALL_MODES) {
      cases.add(new Object[]{mode, Collections.emptyList()});
      cases.add(new Object[]{mode, Arrays.asList(JsonReadFeature.ALLOW_MISSING_VALUES)});
      cases.add(new Object[]{mode, Arrays.asList(JsonReadFeature.ALLOW_TRAILING_COMMA)});
      cases.add(new Object[]{mode, Arrays.asList(JsonReadFeature.ALLOW_MISSING_VALUES,
              JsonReadFeature.ALLOW_TRAILING_COMMA)});
    }

    return cases;
  }

    @MethodSource("getTestCases")
    @ParameterizedTest(name = "Mode {0}, Features {1}")
    void arrayBasic(int mode, List<JsonReadFeature> features) throws Exception {
        initTrailingCommasTest(mode, features);
    String json = "[\"a\", \"b\"]";

    try (JsonParser p = createParser(factory, mode, json)) {

      assertEquals(JsonToken.START_ARRAY, p.nextToken());

      assertToken(JsonToken.VALUE_STRING, p.nextToken());
      assertEquals("a", p.getString());

      assertToken(JsonToken.VALUE_STRING, p.nextToken());
      assertEquals("b", p.getString());

      assertEquals(JsonToken.END_ARRAY, p.nextToken());
      assertEnd(p);
    }
  }

    @MethodSource("getTestCases")
    @ParameterizedTest(name = "Mode {0}, Features {1}")
    void arrayInnerComma(int mode, List<JsonReadFeature> features) throws Exception {
        initTrailingCommasTest(mode, features);
    String json = "[\"a\",, \"b\"]";

    try (JsonParser p = createParser(factory, mode, json)) {

      assertEquals(JsonToken.START_ARRAY, p.nextToken());

      assertToken(JsonToken.VALUE_STRING, p.nextToken());
      assertEquals("a", p.getString());

      if (!features.contains(JsonReadFeature.ALLOW_MISSING_VALUES)) {
        assertUnexpected(p, ',');
        return;
      }

      assertToken(JsonToken.VALUE_NULL, p.nextToken());

      assertToken(JsonToken.VALUE_STRING, p.nextToken());
      assertEquals("b", p.getString());

      assertEquals(JsonToken.END_ARRAY, p.nextToken());
      assertEnd(p);
    }
  }

    @MethodSource("getTestCases")
    @ParameterizedTest(name = "Mode {0}, Features {1}")
    void arrayLeadingComma(int mode, List<JsonReadFeature> features) throws Exception {
        initTrailingCommasTest(mode, features);
    String json = "[,\"a\", \"b\"]";

    try (JsonParser p = createParser(factory, mode, json)) {

      assertEquals(JsonToken.START_ARRAY, p.nextToken());

      if (!features.contains(JsonReadFeature.ALLOW_MISSING_VALUES)) {
        assertUnexpected(p, ',');
        return;
      }

      assertToken(JsonToken.VALUE_NULL, p.nextToken());

      assertToken(JsonToken.VALUE_STRING, p.nextToken());
      assertEquals("a", p.getString());

      assertToken(JsonToken.VALUE_STRING, p.nextToken());
      assertEquals("b", p.getString());

      assertEquals(JsonToken.END_ARRAY, p.nextToken());
      assertEnd(p);
    }
  }

    @MethodSource("getTestCases")
    @ParameterizedTest(name = "Mode {0}, Features {1}")
    void arrayTrailingComma(int mode, List<JsonReadFeature> features) throws Exception {
        initTrailingCommasTest(mode, features);
    String json = "[\"a\", \"b\",]";

    try (JsonParser p = createParser(factory, mode, json)) {

      assertEquals(JsonToken.START_ARRAY, p.nextToken());

      assertToken(JsonToken.VALUE_STRING, p.nextToken());
      assertEquals("a", p.getString());

      assertToken(JsonToken.VALUE_STRING, p.nextToken());
      assertEquals("b", p.getString());

      // ALLOW_TRAILING_COMMA takes priority over ALLOW_MISSING_VALUES
      if (features.contains(JsonReadFeature.ALLOW_TRAILING_COMMA)) {
        assertToken(JsonToken.END_ARRAY, p.nextToken());
        assertEnd(p);
      } else if (features.contains(JsonReadFeature.ALLOW_MISSING_VALUES)) {
        assertToken(JsonToken.VALUE_NULL, p.nextToken());
        assertToken(JsonToken.END_ARRAY, p.nextToken());
        assertEnd(p);
      } else {
        assertUnexpected(p, ']');
      }
    }
  }

    @MethodSource("getTestCases")
    @ParameterizedTest(name = "Mode {0}, Features {1}")
    void arrayTrailingCommas(int mode, List<JsonReadFeature> features) throws Exception {
        initTrailingCommasTest(mode, features);
    String json = "[\"a\", \"b\",,]";

    try (JsonParser p = createParser(factory, mode, json)) {

      assertEquals(JsonToken.START_ARRAY, p.nextToken());

      assertToken(JsonToken.VALUE_STRING, p.nextToken());
      assertEquals("a", p.getString());

      assertToken(JsonToken.VALUE_STRING, p.nextToken());
      assertEquals("b", p.getString());

      // ALLOW_TRAILING_COMMA takes priority over ALLOW_MISSING_VALUES
      if (features.contains(JsonReadFeature.ALLOW_MISSING_VALUES) &&
              features.contains(JsonReadFeature.ALLOW_TRAILING_COMMA)) {
        assertToken(JsonToken.VALUE_NULL, p.nextToken());
        assertToken(JsonToken.END_ARRAY, p.nextToken());
        assertEnd(p);
      } else if (features.contains(JsonReadFeature.ALLOW_MISSING_VALUES)) {
        assertToken(JsonToken.VALUE_NULL, p.nextToken());
        assertToken(JsonToken.VALUE_NULL, p.nextToken());
        assertToken(JsonToken.END_ARRAY, p.nextToken());
        assertEnd(p);
      } else {
        assertUnexpected(p, ',');
      }
    }
  }

    @MethodSource("getTestCases")
    @ParameterizedTest(name = "Mode {0}, Features {1}")
    void arrayTrailingCommasTriple(int mode, List<JsonReadFeature> features) throws Exception {
        initTrailingCommasTest(mode, features);
    String json = "[\"a\", \"b\",,,]";

    try (JsonParser p = createParser(factory, mode, json)) {

      assertEquals(JsonToken.START_ARRAY, p.nextToken());

      assertToken(JsonToken.VALUE_STRING, p.nextToken());
      assertEquals("a", p.getString());

      assertToken(JsonToken.VALUE_STRING, p.nextToken());
      assertEquals("b", p.getString());

      // ALLOW_TRAILING_COMMA takes priority over ALLOW_MISSING_VALUES
      if (features.contains(JsonReadFeature.ALLOW_MISSING_VALUES) &&
              features.contains(JsonReadFeature.ALLOW_TRAILING_COMMA)) {
        assertToken(JsonToken.VALUE_NULL, p.nextToken());
        assertToken(JsonToken.VALUE_NULL, p.nextToken());
        assertToken(JsonToken.END_ARRAY, p.nextToken());
        assertEnd(p);
      } else if (features.contains(JsonReadFeature.ALLOW_MISSING_VALUES)) {
        assertToken(JsonToken.VALUE_NULL, p.nextToken());
        assertToken(JsonToken.VALUE_NULL, p.nextToken());
        assertToken(JsonToken.VALUE_NULL, p.nextToken());
        assertToken(JsonToken.END_ARRAY, p.nextToken());
        assertEnd(p);
      } else {
        assertUnexpected(p, ',');
      }
    }
  }

    @MethodSource("getTestCases")
    @ParameterizedTest(name = "Mode {0}, Features {1}")
    void objectBasic(int mode, List<JsonReadFeature> features) throws Exception {
        initTrailingCommasTest(mode, features);
    String json = "{\"a\": true, \"b\": false}";

    try (JsonParser p = createParser(factory, mode, json)) {
        assertEquals(JsonToken.START_OBJECT, p.nextToken());

        assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
        assertEquals("a", p.getString());
        assertToken(JsonToken.VALUE_TRUE, p.nextToken());
    
        assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
        assertEquals("b", p.getString());
        assertToken(JsonToken.VALUE_FALSE, p.nextToken());

        assertEquals(JsonToken.END_OBJECT, p.nextToken());
        assertEnd(p);
    }
  }

    @MethodSource("getTestCases")
    @ParameterizedTest(name = "Mode {0}, Features {1}")
    void objectInnerComma(int mode, List<JsonReadFeature> features) throws Exception {
        initTrailingCommasTest(mode, features);
    String json = "{\"a\": true,, \"b\": false}";

    try (JsonParser p = createParser(factory, mode, json)) {
        assertEquals(JsonToken.START_OBJECT, p.nextToken());

        assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
        assertEquals("a", p.getString());
        assertToken(JsonToken.VALUE_TRUE, p.nextToken());

        assertUnexpected(p, ',');
    }
  }

    @MethodSource("getTestCases")
    @ParameterizedTest(name = "Mode {0}, Features {1}")
    void objectLeadingComma(int mode, List<JsonReadFeature> features) throws Exception {
        initTrailingCommasTest(mode, features);
    String json = "{,\"a\": true, \"b\": false}";

    try (JsonParser p = createParser(factory, mode, json)) {

      assertEquals(JsonToken.START_OBJECT, p.nextToken());

      assertUnexpected(p, ',');
    }
  }

    @MethodSource("getTestCases")
    @ParameterizedTest(name = "Mode {0}, Features {1}")
    void objectTrailingComma(int mode, List<JsonReadFeature> features) throws Exception {
        initTrailingCommasTest(mode, features);
    String json = "{\"a\": true, \"b\": false,}";

    try (JsonParser p = createParser(factory, mode, json)) {

        assertEquals(JsonToken.START_OBJECT, p.nextToken());

        assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
        assertEquals("a", p.getString());
        assertToken(JsonToken.VALUE_TRUE, p.nextToken());
    
        assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
        assertEquals("b", p.getString());
        assertToken(JsonToken.VALUE_FALSE, p.nextToken());

        if (features.contains(JsonReadFeature.ALLOW_TRAILING_COMMA)) {
            assertToken(JsonToken.END_OBJECT, p.nextToken());
            assertEnd(p);
        } else {
            assertUnexpected(p, '}');
        }
    }
  }

    @MethodSource("getTestCases")
    @ParameterizedTest(name = "Mode {0}, Features {1}")
    void objectTrailingCommaWithNextFieldName(int mode, List<JsonReadFeature> features) throws Exception {
        initTrailingCommasTest(mode, features);
    String json = "{\"a\": true, \"b\": false,}";

    try (JsonParser p = createParser(factory, mode, json)) {
        assertEquals(JsonToken.START_OBJECT, p.nextToken());
        assertEquals("a", p.nextName());
        assertToken(JsonToken.VALUE_TRUE, p.nextToken());
    
        assertEquals("b", p.nextName());
        assertToken(JsonToken.VALUE_FALSE, p.nextToken());
    
        if (features.contains(JsonReadFeature.ALLOW_TRAILING_COMMA)) {
          assertEquals(null, p.nextName());
          assertToken(JsonToken.END_OBJECT, p.currentToken());
          assertEnd(p);
        } else {
          try {
            p.nextName();
            fail("No exception thrown");
          } catch (Exception e) {
            verifyException(e, "Unexpected character ('}' (code 125))");
          }
        }
    }
    }

    @MethodSource("getTestCases")
    @ParameterizedTest(name = "Mode {0}, Features {1}")
    void objectTrailingCommaWithNextFieldNameStr(int mode, List<JsonReadFeature> features) throws Exception {
        initTrailingCommasTest(mode, features);
    String json = "{\"a\": true, \"b\": false,}";

    try (JsonParser p = createParser(factory, mode, json)) {

      assertEquals(JsonToken.START_OBJECT, p.nextToken());

        assertTrue(p.nextName(new SerializedString("a")));
        assertToken(JsonToken.VALUE_TRUE, p.nextToken());
    
        assertTrue(p.nextName(new SerializedString("b")));
        assertToken(JsonToken.VALUE_FALSE, p.nextToken());
    
        if (features.contains(JsonReadFeature.ALLOW_TRAILING_COMMA)) {
          assertFalse(p.nextName(new SerializedString("c")));
          assertToken(JsonToken.END_OBJECT, p.currentToken());
          assertEnd(p);
        } else {
          try {
            p.nextName(new SerializedString("c"));
            fail("No exception thrown");
          } catch (Exception e) {
            verifyException(e, "Unexpected character ('}' (code 125))");
          }
        }
    }
  }

    @MethodSource("getTestCases")
    @ParameterizedTest(name = "Mode {0}, Features {1}")
    void objectTrailingCommas(int mode, List<JsonReadFeature> features) throws Exception {
        initTrailingCommasTest(mode, features);
    String json = "{\"a\": true, \"b\": false,,}";

    try (JsonParser p = createParser(factory, mode, json)) {
        assertEquals(JsonToken.START_OBJECT, p.nextToken());

        assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
        assertEquals("a", p.getString());
        assertToken(JsonToken.VALUE_TRUE, p.nextToken());
    
        assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
        assertEquals("b", p.getString());
        assertToken(JsonToken.VALUE_FALSE, p.nextToken());

        assertUnexpected(p, ',');
    }
  }

  private void assertEnd(JsonParser p) throws IOException {
    // Issue #325
    if (!(p instanceof UTF8DataInputJsonParser)) {
      JsonToken next = p.nextToken();
      assertNull(next, "expected end of stream but found " + next);
    }
  }

  private void assertUnexpected(JsonParser p, char c) throws IOException {
    try {
      p.nextToken();
      fail("No exception thrown");
    } catch (Exception e) {
      verifyException(e, String.format("Unexpected character ('%s' (code %d))", c, (int) c));
    }
  }
}
